//
//  APIProvider.swift
//  MSAPI
//
//  Created by mrsong on 2020/4/15.
//

import Foundation

public protocol APIPluginType {
    func prepare(_ request: URLRequest, targetType: TargetType) -> URLRequest
    func willSend(_ request: URLRequest, targetType: TargetType)
    func didReceive(_ response: APIResponse, targetType: TargetType)
    func process(_ response: APIResponse, targetType: TargetType) -> APIResponse
}

public extension APIPluginType {
    func prepare(_ request: URLRequest, targetType: TargetType) -> URLRequest { request }
    func willSend(_ request: URLRequest, targetType: TargetType) {}
    func didReceive(_ response: APIResponse, targetType: TargetType) {}
    func process(_ response: APIResponse, targetType: TargetType) -> APIResponse { response }
}

public enum APIResult<T: Decodable> {
    case success(data: T?)
    case failure(error: APIError)
}

public struct APIResponse {
    public let data: Data?
    public let urlResponse: URLResponse?
    public let error: Error?
}

public struct APIError: Error {
    public let code: Code
    public let description: String
    public let data: Data?
    
    public enum Code {
        case error(Error)
        case status(Int)
        case decoding(Decoding)
        
        public enum Decoding {
            case dataCorrupted
            case keyNotFound
            case typeMismatch
            case valueNotFound
            case unknown
        }
    }
}

public typealias APICompletion<T: Decodable> = (URLResponse?, APIResult<T>) -> Void

public let defalutAPISession = URLSession(configuration: .ephemeral)
public let defalutJSONDecoder = JSONDecoder()

public class APIProvider<Target: TargetType> {
    
    private let session: URLSession
    private let plugins: [APIPluginType]
    
    public init(session: URLSession = defalutAPISession, plugins: [APIPluginType] = []) {
        self.session = session
        self.plugins = plugins
    }
        
    public func request<T: Decodable>(_ target: Target, completion: @escaping APICompletion<T>) {
        let urlRequest = plugins.reduce(target.urlRequest()) { $1.prepare($0, targetType: target) }
        plugins.forEach { $0.willSend(urlRequest, targetType: target) }
        session.dataTask(with: urlRequest) { (data, response, error) in
            let rawResponse = APIResponse(data: data, urlResponse: response, error: error)
            self.plugins.forEach { $0.didReceive(rawResponse, targetType: target) }
            let finalResponse = self.plugins.reduce(rawResponse) { $1.process($0, targetType: target) }
            self.handleResponse(finalResponse, completion: completion)
        }.resume()
    }
    
    private func handleResponse<T: Decodable>(_ response: APIResponse, completion: @escaping APICompletion<T>) {
        let (data, response, error) = (response.data, response.urlResponse, response.error)
        // failure for error
        if let error = error {
            completion(response, .failure(error: .init(code: .error(error), description: (error as NSError).description, data: data)))
            return
        }
        // failure for status code
        let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
        if !(200...299).contains(statusCode) {
            completion(response, .failure(error: .init(code: .status(statusCode), description: "Status code is not in the range of 200 to 299", data: data)))
        }
        // API success
        if let data = data {
            var decodingError: APIError?
            do {
                let object = try defalutJSONDecoder.decode(T.self, from: data)
                completion(response, .success(data: object))
            } catch DecodingError.dataCorrupted(let context) {
                decodingError = .init(code: .decoding(.dataCorrupted), description: context.debugDescription, data: data)
            } catch DecodingError.keyNotFound(let key, let context) {
                decodingError = .init(code: .decoding(.keyNotFound), description: "\(key.stringValue) was not found, \(context.debugDescription)", data: data)
            } catch DecodingError.typeMismatch(let type, let context) {
                decodingError = .init(code: .decoding(.typeMismatch), description: "\(type) mismatch, \(context.debugDescription)", data: data)
            } catch DecodingError.valueNotFound(let type, let context) {
                decodingError = .init(code: .decoding(.valueNotFound), description: "value not found for \(type), \(context.debugDescription)", data: data)
            } catch {
                decodingError = .init(code: .decoding(.unknown), description: "Unknown parse error", data: data)
            }
            
            if let error = decodingError {
                completion(response, .failure(error: error))
            }
        } else {
            completion(response, .success(data: nil))
        }
    }
}
